// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: http://codemirror.net/LICENSE

(function(mod) {
  if (typeof exports == 'object' && typeof module == 'object')
    // CommonJS
    mod(require('../../lib/codemirror'));
  else if (typeof define == 'function' && define.amd)
    // AMD
    define(['../../lib/codemirror'], mod);
  // Plain browser env
  else mod(CodeMirror);
})(function(CodeMirror) {
  'use strict';

  CodeMirror.defineMode('haxe', function(config, parserConfig) {
    var indentUnit = config.indentUnit;

    // Tokenizer

    function kw(type) {
      return { type: type, style: 'keyword' };
    }
    var A = kw('keyword a'),
      B = kw('keyword b'),
      C = kw('keyword c');
    var operator = kw('operator'),
      atom = { type: 'atom', style: 'atom' },
      attribute = { type: 'attribute', style: 'attribute' };
    var type = kw('typedef');
    var keywords = {
      if: A,
      while: A,
      else: B,
      do: B,
      try: B,
      return: C,
      break: C,
      continue: C,
      new: C,
      throw: C,
      var: kw('var'),
      inline: attribute,
      static: attribute,
      using: kw('import'),
      public: attribute,
      private: attribute,
      cast: kw('cast'),
      import: kw('import'),
      macro: kw('macro'),
      function: kw('function'),
      catch: kw('catch'),
      untyped: kw('untyped'),
      callback: kw('cb'),
      for: kw('for'),
      switch: kw('switch'),
      case: kw('case'),
      default: kw('default'),
      in: operator,
      never: kw('property_access'),
      trace: kw('trace'),
      class: type,
      abstract: type,
      enum: type,
      interface: type,
      typedef: type,
      extends: type,
      implements: type,
      dynamic: type,
      true: atom,
      false: atom,
      null: atom,
    };

    var isOperatorChar = /[+\-*&%=<>!?|]/;

    function chain(stream, state, f) {
      state.tokenize = f;
      return f(stream, state);
    }

    function toUnescaped(stream, end) {
      var escaped = false,
        next;
      while ((next = stream.next()) != null) {
        if (next == end && !escaped) return true;
        escaped = !escaped && next == '\\';
      }
    }

    // Used as scratch variables to communicate multiple values without
    // consing up tons of objects.
    var type, content;
    function ret(tp, style, cont) {
      type = tp;
      content = cont;
      return style;
    }

    function haxeTokenBase(stream, state) {
      var ch = stream.next();
      if (ch == '"' || ch == "'") {
        return chain(stream, state, haxeTokenString(ch));
      } else if (/[\[\]{}\(\),;\:\.]/.test(ch)) {
        return ret(ch);
      } else if (ch == '0' && stream.eat(/x/i)) {
        stream.eatWhile(/[\da-f]/i);
        return ret('number', 'number');
      } else if (/\d/.test(ch) || (ch == '-' && stream.eat(/\d/))) {
        stream.match(/^\d*(?:\.\d*(?!\.))?(?:[eE][+\-]?\d+)?/);
        return ret('number', 'number');
      } else if (state.reAllowed && ch == '~' && stream.eat(/\//)) {
        toUnescaped(stream, '/');
        stream.eatWhile(/[gimsu]/);
        return ret('regexp', 'string-2');
      } else if (ch == '/') {
        if (stream.eat('*')) {
          return chain(stream, state, haxeTokenComment);
        } else if (stream.eat('/')) {
          stream.skipToEnd();
          return ret('comment', 'comment');
        } else {
          stream.eatWhile(isOperatorChar);
          return ret('operator', null, stream.current());
        }
      } else if (ch == '#') {
        stream.skipToEnd();
        return ret('conditional', 'meta');
      } else if (ch == '@') {
        stream.eat(/:/);
        stream.eatWhile(/[\w_]/);
        return ret('metadata', 'meta');
      } else if (isOperatorChar.test(ch)) {
        stream.eatWhile(isOperatorChar);
        return ret('operator', null, stream.current());
      } else {
        var word;
        if (/[A-Z]/.test(ch)) {
          stream.eatWhile(/[\w_<>]/);
          word = stream.current();
          return ret('type', 'variable-3', word);
        } else {
          stream.eatWhile(/[\w_]/);
          var word = stream.current(),
            known = keywords.propertyIsEnumerable(word) && keywords[word];
          return known && state.kwAllowed
            ? ret(known.type, known.style, word)
            : ret('variable', 'variable', word);
        }
      }
    }

    function haxeTokenString(quote) {
      return function(stream, state) {
        if (toUnescaped(stream, quote)) state.tokenize = haxeTokenBase;
        return ret('string', 'string');
      };
    }

    function haxeTokenComment(stream, state) {
      var maybeEnd = false,
        ch;
      while ((ch = stream.next())) {
        if (ch == '/' && maybeEnd) {
          state.tokenize = haxeTokenBase;
          break;
        }
        maybeEnd = ch == '*';
      }
      return ret('comment', 'comment');
    }

    // Parser

    var atomicTypes = {
      atom: true,
      number: true,
      variable: true,
      string: true,
      regexp: true,
    };

    function HaxeLexical(indented, column, type, align, prev, info) {
      this.indented = indented;
      this.column = column;
      this.type = type;
      this.prev = prev;
      this.info = info;
      if (align != null) this.align = align;
    }

    function inScope(state, varname) {
      for (var v = state.localVars; v; v = v.next)
        if (v.name == varname) return true;
    }

    function parseHaxe(state, style, type, content, stream) {
      var cc = state.cc;
      // Communicate our context to the combinators.
      // (Less wasteful than consing up a hundred closures on every call.)
      cx.state = state;
      cx.stream = stream;
      (cx.marked = null), (cx.cc = cc);

      if (!state.lexical.hasOwnProperty('align')) state.lexical.align = true;

      while (true) {
        var combinator = cc.length ? cc.pop() : statement;
        if (combinator(type, content)) {
          while (cc.length && cc[cc.length - 1].lex) cc.pop()();
          if (cx.marked) return cx.marked;
          if (type == 'variable' && inScope(state, content))
            return 'variable-2';
          if (type == 'variable' && imported(state, content))
            return 'variable-3';
          return style;
        }
      }
    }

    function imported(state, typename) {
      if (/[a-z]/.test(typename.charAt(0))) return false;
      var len = state.importedtypes.length;
      for (var i = 0; i < len; i++)
        if (state.importedtypes[i] == typename) return true;
    }

    function registerimport(importname) {
      var state = cx.state;
      for (var t = state.importedtypes; t; t = t.next)
        if (t.name == importname) return;
      state.importedtypes = { name: importname, next: state.importedtypes };
    }
    // Combinator utils

    var cx = { state: null, column: null, marked: null, cc: null };
    function pass() {
      for (var i = arguments.length - 1; i >= 0; i--) cx.cc.push(arguments[i]);
    }
    function cont() {
      pass.apply(null, arguments);
      return true;
    }
    function inList(name, list) {
      for (var v = list; v; v = v.next) if (v.name == name) return true;
      return false;
    }
    function register(varname) {
      var state = cx.state;
      if (state.context) {
        cx.marked = 'def';
        if (inList(varname, state.localVars)) return;
        state.localVars = { name: varname, next: state.localVars };
      } else if (state.globalVars) {
        if (inList(varname, state.globalVars)) return;
        state.globalVars = { name: varname, next: state.globalVars };
      }
    }

    // Combinators

    var defaultVars = { name: 'this', next: null };
    function pushcontext() {
      if (!cx.state.context) cx.state.localVars = defaultVars;
      cx.state.context = { prev: cx.state.context, vars: cx.state.localVars };
    }
    function popcontext() {
      cx.state.localVars = cx.state.context.vars;
      cx.state.context = cx.state.context.prev;
    }
    popcontext.lex = true;
    function pushlex(type, info) {
      var result = function() {
        var state = cx.state;
        state.lexical = new HaxeLexical(
          state.indented,
          cx.stream.column(),
          type,
          null,
          state.lexical,
          info,
        );
      };
      result.lex = true;
      return result;
    }
    function poplex() {
      var state = cx.state;
      if (state.lexical.prev) {
        if (state.lexical.type == ')') state.indented = state.lexical.indented;
        state.lexical = state.lexical.prev;
      }
    }
    poplex.lex = true;

    function expect(wanted) {
      function f(type) {
        if (type == wanted) return cont();
        else if (wanted == ';') return pass();
        else return cont(f);
      }
      return f;
    }

    function statement(type) {
      if (type == '@') return cont(metadef);
      if (type == 'var')
        return cont(pushlex('vardef'), vardef1, expect(';'), poplex);
      if (type == 'keyword a')
        return cont(pushlex('form'), expression, statement, poplex);
      if (type == 'keyword b') return cont(pushlex('form'), statement, poplex);
      if (type == '{')
        return cont(pushlex('}'), pushcontext, block, poplex, popcontext);
      if (type == ';') return cont();
      if (type == 'attribute') return cont(maybeattribute);
      if (type == 'function') return cont(functiondef);
      if (type == 'for')
        return cont(
          pushlex('form'),
          expect('('),
          pushlex(')'),
          forspec1,
          expect(')'),
          poplex,
          statement,
          poplex,
        );
      if (type == 'variable') return cont(pushlex('stat'), maybelabel);
      if (type == 'switch')
        return cont(
          pushlex('form'),
          expression,
          pushlex('}', 'switch'),
          expect('{'),
          block,
          poplex,
          poplex,
        );
      if (type == 'case') return cont(expression, expect(':'));
      if (type == 'default') return cont(expect(':'));
      if (type == 'catch')
        return cont(
          pushlex('form'),
          pushcontext,
          expect('('),
          funarg,
          expect(')'),
          statement,
          poplex,
          popcontext,
        );
      if (type == 'import') return cont(importdef, expect(';'));
      if (type == 'typedef') return cont(typedef);
      return pass(pushlex('stat'), expression, expect(';'), poplex);
    }
    function expression(type) {
      if (atomicTypes.hasOwnProperty(type)) return cont(maybeoperator);
      if (type == 'type') return cont(maybeoperator);
      if (type == 'function') return cont(functiondef);
      if (type == 'keyword c') return cont(maybeexpression);
      if (type == '(')
        return cont(
          pushlex(')'),
          maybeexpression,
          expect(')'),
          poplex,
          maybeoperator,
        );
      if (type == 'operator') return cont(expression);
      if (type == '[')
        return cont(
          pushlex(']'),
          commasep(maybeexpression, ']'),
          poplex,
          maybeoperator,
        );
      if (type == '{')
        return cont(
          pushlex('}'),
          commasep(objprop, '}'),
          poplex,
          maybeoperator,
        );
      return cont();
    }
    function maybeexpression(type) {
      if (type.match(/[;\}\)\],]/)) return pass();
      return pass(expression);
    }

    function maybeoperator(type, value) {
      if (type == 'operator' && /\+\+|--/.test(value))
        return cont(maybeoperator);
      if (type == 'operator' || type == ':') return cont(expression);
      if (type == ';') return;
      if (type == '(')
        return cont(
          pushlex(')'),
          commasep(expression, ')'),
          poplex,
          maybeoperator,
        );
      if (type == '.') return cont(property, maybeoperator);
      if (type == '[')
        return cont(
          pushlex(']'),
          expression,
          expect(']'),
          poplex,
          maybeoperator,
        );
    }

    function maybeattribute(type) {
      if (type == 'attribute') return cont(maybeattribute);
      if (type == 'function') return cont(functiondef);
      if (type == 'var') return cont(vardef1);
    }

    function metadef(type) {
      if (type == ':') return cont(metadef);
      if (type == 'variable') return cont(metadef);
      if (type == '(')
        return cont(pushlex(')'), commasep(metaargs, ')'), poplex, statement);
    }
    function metaargs(type) {
      if (type == 'variable') return cont();
    }

    function importdef(type, value) {
      if (type == 'variable' && /[A-Z]/.test(value.charAt(0))) {
        registerimport(value);
        return cont();
      } else if (type == 'variable' || type == 'property' || type == '.' || value == '*') return cont(importdef);
    }

    function typedef(type, value) {
      if (type == 'variable' && /[A-Z]/.test(value.charAt(0))) {
        registerimport(value);
        return cont();
      } else if (type == 'type' && /[A-Z]/.test(value.charAt(0))) {
        return cont();
      }
    }

    function maybelabel(type) {
      if (type == ':') return cont(poplex, statement);
      return pass(maybeoperator, expect(';'), poplex);
    }
    function property(type) {
      if (type == 'variable') {
        cx.marked = 'property';
        return cont();
      }
    }
    function objprop(type) {
      if (type == 'variable') cx.marked = 'property';
      if (atomicTypes.hasOwnProperty(type))
        return cont(expect(':'), expression);
    }
    function commasep(what, end) {
      function proceed(type) {
        if (type == ',') return cont(what, proceed);
        if (type == end) return cont();
        return cont(expect(end));
      }
      return function(type) {
        if (type == end) return cont();
        else return pass(what, proceed);
      };
    }
    function block(type) {
      if (type == '}') return cont();
      return pass(statement, block);
    }
    function vardef1(type, value) {
      if (type == 'variable') {
        register(value);
        return cont(typeuse, vardef2);
      }
      return cont();
    }
    function vardef2(type, value) {
      if (value == '=') return cont(expression, vardef2);
      if (type == ',') return cont(vardef1);
    }
    function forspec1(type, value) {
      if (type == 'variable') {
        register(value);
        return cont(forin, expression);
      } else {
        return pass();
      }
    }
    function forin(_type, value) {
      if (value == 'in') return cont();
    }
    function functiondef(type, value) {
      //function names starting with upper-case letters are recognised as types, so cludging them together here.
      if (type == 'variable' || type == 'type') {
        register(value);
        return cont(functiondef);
      }
      if (value == 'new') return cont(functiondef);
      if (type == '(')
        return cont(
          pushlex(')'),
          pushcontext,
          commasep(funarg, ')'),
          poplex,
          typeuse,
          statement,
          popcontext,
        );
    }
    function typeuse(type) {
      if (type == ':') return cont(typestring);
    }
    function typestring(type) {
      if (type == 'type') return cont();
      if (type == 'variable') return cont();
      if (type == '{')
        return cont(pushlex('}'), commasep(typeprop, '}'), poplex);
    }
    function typeprop(type) {
      if (type == 'variable') return cont(typeuse);
    }
    function funarg(type, value) {
      if (type == 'variable') {
        register(value);
        return cont(typeuse);
      }
    }

    // Interface
    return {
      startState: function(basecolumn) {
        var defaulttypes = [
          'Int',
          'Float',
          'String',
          'Void',
          'Std',
          'Bool',
          'Dynamic',
          'Array',
        ];
        var state = {
          tokenize: haxeTokenBase,
          reAllowed: true,
          kwAllowed: true,
          cc: [],
          lexical: new HaxeLexical(
            (basecolumn || 0) - indentUnit,
            0,
            'block',
            false,
          ),
          localVars: parserConfig.localVars,
          importedtypes: defaulttypes,
          context: parserConfig.localVars && { vars: parserConfig.localVars },
          indented: 0,
        };
        if (
          parserConfig.globalVars &&
          typeof parserConfig.globalVars == 'object'
        )
          state.globalVars = parserConfig.globalVars;
        return state;
      },

      token: function(stream, state) {
        if (stream.sol()) {
          if (!state.lexical.hasOwnProperty('align'))
            state.lexical.align = false;
          state.indented = stream.indentation();
        }
        if (stream.eatSpace()) return null;
        var style = state.tokenize(stream, state);
        if (type == 'comment') return style;
        state.reAllowed = !!(
          type == 'operator' ||
          type == 'keyword c' ||
          type.match(/^[\[{}\(,;:]$/)
        );
        state.kwAllowed = type != '.';
        return parseHaxe(state, style, type, content, stream);
      },

      indent: function(state, textAfter) {
        if (state.tokenize != haxeTokenBase) return 0;
        var firstChar = textAfter && textAfter.charAt(0),
          lexical = state.lexical;
        if (lexical.type == 'stat' && firstChar == '}') lexical = lexical.prev;
        var type = lexical.type,
          closing = firstChar == type;
        if (type == 'vardef') return lexical.indented + 4;
        else if (type == 'form' && firstChar == '{') return lexical.indented;
        else if (type == 'stat' || type == 'form')
          return lexical.indented + indentUnit;
        else if (lexical.info == 'switch' && !closing)
          return (
            lexical.indented +
            (/^(?:case|default)\b/.test(textAfter)
              ? indentUnit
              : 2 * indentUnit)
          );
        else if (lexical.align) return lexical.column + (closing ? 0 : 1);
        else return lexical.indented + (closing ? 0 : indentUnit);
      },

      electricChars: '{}',
      blockCommentStart: '/*',
      blockCommentEnd: '*/',
      lineComment: '//',
    };
  });

  CodeMirror.defineMIME('text/x-haxe', 'haxe');

  CodeMirror.defineMode('hxml', function() {
    return {
      startState: function() {
        return {
          define: false,
          inString: false,
        };
      },
      token: function(stream, state) {
        var ch = stream.peek();
        var sol = stream.sol();

        ///* comments */
        if (ch == '#') {
          stream.skipToEnd();
          return 'comment';
        }
        if (sol && ch == '-') {
          var style = 'variable-2';

          stream.eat(/-/);

          if (stream.peek() == '-') {
            stream.eat(/-/);
            style = 'keyword a';
          }

          if (stream.peek() == 'D') {
            stream.eat(/[D]/);
            style = 'keyword c';
            state.define = true;
          }

          stream.eatWhile(/[A-Z]/i);
          return style;
        }

        var ch = stream.peek();

        if (state.inString == false && ch == "'") {
          state.inString = true;
          stream.next();
        }

        if (state.inString == true) {
          if (stream.skipTo("'")) {
          } else {
            stream.skipToEnd();
          }

          if (stream.peek() == "'") {
            stream.next();
            state.inString = false;
          }

          return 'string';
        }

        stream.next();
        return null;
      },
      lineComment: '#',
    };
  });

  CodeMirror.defineMIME('text/x-hxml', 'hxml');
});
